В предыдущей серии я описал как я замерял скорость работы своей библиотеки и заметил странную разницу при работе с константами и вычитанием, которой не было в нативном коде. Я даже открыл тикет на эту тему. И вот там-то в ходе обсуждения мне @mikedn напомнил интересную тему про микробенчмарки: знай, что ты тестируешь. Чем больше кода используется в бенчмарке и чем больше условий, тем больше шансов ошибиться и измерить что-нибудь не то. Например, в этом тикете про скорость коннектора, люди измеряют не столько скорость работы коннектора, сколько скорость работы сборщика мусора в .net.
Я наткнулся на похожую историю. Ниже результаты правильного бенчмарка:
Method | Mean | Error | StdDev | Q3 | Scaled | ScaledSD | Allocated |
---|---|---|---|---|---|---|---|
Span | 378.8 ns | 6.778 ns | 6.340 ns | 385.2 ns | 2.01 | 0.04 | 0 B |
SpanConst | 198.7 ns | 1.255 ns | 1.174 ns | 199.5 ns | 1.06 | 0.02 | 0 B |
Pointer | 235.5 ns | 4.583 ns | 4.501 ns | 237.6 ns | 1.25 | 0.03 | 0 B |
C | 188.1 ns | 2.714 ns | 2.538 ns | 190.3 ns | 1.00 | 0.00 | 0 B |
Cpp | 189.5 ns | 2.896 ns | 2.709 ns | 191.5 ns | 1.01 | 0.02 | 0 B |
Здесь Span
и SpanConst
бенчмарки выполняют сериализацию, используя Span<T>
, только второй сериализует константу, а первый - нет. Остальные называются аналогично предыдущему посту.
Давайте пока отложим SpanConst
в сторону. Как мы видим, Span
результат ухудшился незначительно, а вот все остальные - значительно, ровно в два раза. Почему так? Предыдущий бенчмарк сериализовал либо одно большое число, либо серию больших чисел, но все они лежат в отрезке num ∈ [1<<30 - 100, 1<<30]
. А новый берёт число 99000 и на каждой итерации вычитает одну тысячу, пока не достигнет нуля. Посмотрим на код метода mp_encode_uint
в msgpuck (библиотечные методы в других библиотеках выглядят аналогично):
В старом бенчмарке GCC 6.0 смог догадаться, что num ∈ [1<<30 - 100, 1<<30]
даже для не-константы и выпилил все ветки, кроме одной. В .net core JIT смог это сделать только для случая с константой. В остальных случаях jit генерировал полный код для прохода всех веток. Поэтому “замедление” было на самом деле честной работой кода. А вот быстрая работа - это была аномалия, связанная с тем, что иногда компилятор выкинет лишний код, если будет уверен в его ненужности и ваша программа станет быстрее. Это следует не забывать и учитывать при проектировании бенчмарка. Например, в данном бенчмарке присутствует SpanConst
метод. Его задача продемонстрировать истинность гипотезы о том, что jit и gcc выкидывают код.
Вывод из всего этого можно сделать простой: бенчмарки - это сложно, надо знать, что ты замеряешь и мерить, в том числе, и граничные условия.
comments powered by Disqus